fix(core): give readWorkspace ignore rules, size caps and a skip report - #24
Merged
Conversation
Pointing `workspace` at a real Claude Code project walked `node_modules`,
`.git` and every build artifact into memory, and since the UTF-8 guard
landed one `.git` pack or icon among them failed the entire seed — so the
use case this file's own header states as the reason it exists could not
run at all.
Ignore rules come from git: `git ls-files -co --exclude-standard -z` in
the source root is exactly the tracked plus untracked-not-ignored set,
which makes the project's own maintained `.gitignore` the source of truth
and leaves us no gitignore parser to disagree with git about. A source
git cannot describe falls back to a short built-in deny list.
A binary that survives the ignore rules is now skipped and reported
rather than fatal. Before the rules existed, refusing was right because
the file had only been swept up by a broad walk; after them it is real
project content — an icon, a font — that simply cannot cross a text-only
contract, and failing the seed over it helps nobody.
Size caps are internal constants, not options nobody has asked for: 1 MiB
per file is reported like a binary, and 64 MiB in total throws, because a
seed that large is a mistake about which directory was handed over and
silence would be worse than a stop.
BREAKING CHANGE: `readWorkspace` returns `{ files, skipped }` rather than
`WorkspaceFiles`. Reporting a skipped file has to mean something the
caller can observe, and this package has no logger to write it to.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
amondnet
marked this pull request as ready for review
August 28, 2026 18:30
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Architecture diagram
sequenceDiagram
participant Agent as defineAgent
participant RW as readWorkspace
participant Git as git CLI
participant FS as node:fs/promises
participant CP as node:child_process
participant Seed as seedWorkspace
Note over Agent,RW: Workspace resolution flow
Agent->>RW: readWorkspace(source)
alt Source is inlined WorkspaceFiles
RW-->>Agent: { files: source, skipped: [] }
else Source is path or URL
RW->>RW: Resolve root (URL -> fileURLToPath)
RW->>Git: git ls-files -co --exclude-standard -z (cwd=root)
alt Git succeeds
Git-->>RW: Relative file candidates
else Git fails (no repo, no git, unsafe dir)
RW->>FS: readdir(root, { recursive: true })
FS-->>RW: All entries
RW->>RW: Filter by WORKSPACE_IGNORED_DIRECTORIES
end
RW->>FS: stat each candidate
loop For each surviving file
alt File missing or not regular
FS-->>RW: Skip silently
else File size > MAX_WORKSPACE_FILE_BYTES
RW->>RW: Record skipped { path, reason: 'too-large' }
else Total accumulated > MAX_WORKSPACE_TOTAL_BYTES
RW-->>Agent: Throw RangeError
else File decodes as valid UTF-8
FS-->>RW: Read bytes
RW->>RW: decodeText() with fatal TextDecoder
RW->>RW: Store in files[normalizedPath]
else Decode fails (binary content)
RW->>RW: Record skipped { path, reason: 'binary' }
end
end
RW-->>Agent: { files, skipped }
end
Agent->>Seed: seedWorkspace(session, sessionWorkDir, files)
Seed-->>Agent: Seeded workspace files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…degrading `gitCandidates` treated every `git ls-files` failure as "not a repository". An output limit is not that: `execFile` rejects with `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` when a real repository's file list overruns `maxBuffer`, and the fallback walk honours no `.gitignore` — so the guard added to keep `node_modules` out was the one path that would start carrying it. That error now raises. `readCandidates` dropped any path `stat` refused. Only a vanished entry is nothing to read; a permission denial is a file the caller asked for and did not get, so it is raised rather than dropped into the silence the `skipped` report exists to remove.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
readWorkspacewalked the source with a bare recursivereaddirand read every file into a string. There was no ignore list and no size cap, so pointingworkspaceat a real Claude Code project walkednode_modules,.gitand every build artifact into memory — and since the UTF-8 guard landed, one.gitpack or icon among them failed the entire seed. The use caseworkspace.ts's own header andREADME.mdstate as the reason it exists could not run at all.Ignore rules come from git.
git ls-files -co --exclude-standard -zwithcwdat the source root is exactly the tracked plus untracked-not-ignored set: the project's own maintained.gitignoredecides what travels, and there is no gitignore parser of ours to disagree with git about.-zso a filename containing a newline survives; the paths come out relative to the cwd, which is the shapeWorkspaceFileskeys already use.A non-git source falls back to a short built-in deny list (
WORKSPACE_IGNORED_DIRECTORIES:.git,node_modules,dist,build,coverage,.next,.turbo,.cache,out). It is a fallback, not the primary mechanism.A binary that survives the ignore rules is skipped and reported, not fatal. Before the rules existed, refusing was right — the file had only been swept up by a broad walk. After them it is real project content that cannot cross a text-only contract, and failing the seed over an icon helps nobody.
decodeText's detection is unchanged; only what happens on failure is.Size caps are internal constants, not new options. 1 MiB per file is reported like a binary; 64 MiB in total throws, because a seed that large is a mistake about which directory was handed over.
node:child_processis reached through a dynamic import, exactly asnode:fsandnode:urlalready are, so a Worker bundle that only ever passes an inlined record does not pull the host process surface in. TheWorkspaceFilesinput path still short-circuits before any of it.Breaking change
readWorkspacenow returns{ files, skipped }instead ofWorkspaceFiles;skippedis{ path, reason: 'binary' | 'too-large' }[]. "Reported" has to mean observable, and a callback would put logging in a package that has no logger.defineAgentis updated;seedWorkspacestill takesWorkspaceFilesand is unaffected.Related issue
Closes #19
Checklist
bun run test) — 206 pass, up from 202bun run lint,bun run type-check)BREAKING CHANGE:note is includedSummary by cubic
Fixes
readWorkspaceso pointing it at a real project no longer sweepsnode_modules,.git, and build artifacts into memory, and one binary file no longer fails the entire seed. It now honors the project's own git ignore rules (git ls-files -co --exclude-standard), falls back to a short built-in deny list when the source is not a git repo, and skips and reports binary or oversized files instead of throwing. Size caps (1 MiB per file, 64 MiB total) are enforced, and two failures raise instead of degrading: a repo whose file list overruns the buffer, and a listed file that cannot be stat-ed for any reason other than absence.Breaking change
readWorkspacenow returns{ files, skipped }instead ofWorkspaceFiles;skippedentries carryreason: 'binary' | 'too-large'.defineAgentis updated for the new shape;seedWorkspacestill takesWorkspaceFilesunchanged.Written for commit 1c87e47. Summary will update on new commits.